/*
 * Copyright (c) 2008 Allurent, Inc.
 * http://www.allurent.com/
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.allurent.database
{
    import flash.data.SQLConnection;
    import flash.data.SQLMode;
    import flash.events.SQLErrorEvent;
    import flash.events.SQLEvent;
    import flash.filesystem.File;
    import flash.utils.Dictionary;
    
    import mx.logging.ILogger;
    import mx.logging.Log;
    
    /**
     * Manages a pool of connections to the supplied database.
     * It is up to the calling code to return a connection once finished with it.
     * No attempt is made to shrink the connection pool or test that a SQLConnection
     * is still valid before handing it off to the calling code.
     * The connections are returned in a round robin manner which when used
     * with the StatementPool may slightly increase memory as the same query
     * maybe associated with multiple different SQLStatement/SQLConnection pairs.
     */  
    public class ConnectionPool
    {
        private static var _myLogger:ILogger = Log.getLogger("com.allurent.database.ConnectionPool");

        // The maximum size the pool can grow to.
        //參考下面openConnection()的說明，因為開啟資料庫的方式改為UPDATE，所以為了避免發生衝突，同時間只允許一個連線。
        private static const MAX_SIZE:int = 1;
        // The database to connect to.
        private var _database:File;
        // Pool of connections that have been made.
        private var _pool:Array = new Array();
        // Queue of requests to get a connection.
        private var _pending:Array = new Array();
        // Number of connections that have been given out for use.
        private var _useCount:int = 0;
        // Track in use connections.
        private var _inUse:Dictionary = new Dictionary();
        
        /**
         * Create a new connection pool that opens connections
         * to the given database.
         * @param database Database to open connections to
         */  
        public function ConnectionPool(database:File)
        {
            _database = database;
            // Seed the pool with one connection.
            openConnection(null);
        }
        
        /**
         * Reset the connection pool. This will close all connections,
         * remove any pending operations, and cancel any in progress
         * operations. 
         */
        public function reset():void
        {
            _pending = new Array();
            _useCount = 0;
            var sqlConnection:SQLConnection;
            for each (sqlConnection in _pool)
            {
                closeConnection(sqlConnection);
            }
            _pool = new Array();
            for each (sqlConnection in _inUse)
            {
                closeConnection(sqlConnection);
            }
            _inUse = new Dictionary();
        }
        
        /**
         * Number of connections that are actively being used.
         * @return Number of active connections
         */ 
        public function get activeCount():int
        {
            return _useCount;
        }
        
        /**
         * Number of connections that are sitting in the pool.
         * @return Number of inactive connections
         */
        public function get inactiveCount():int
        {
            return _pool.length;
        }
        
        /**
         * Request a connection from the pool. When the connection is
         * available it will be passed to the handler function.
         * @param handler Function to execute when connection is available
         */  
        public function getConnection(handler:Function):void
        {
            // An enhancement of this method would define an optional
            // second parameter that represents the priority of the
            // connection request. Requests with a higher priority
            // would be processed before standard priority requests
            // if the number of available connections was not
            // enough to handle all connection requests.
            _myLogger.debug("Connection requested. Using {0} of {1}.", _useCount, _pool.length);
            _pending.push(handler);
            processPending();
        }
        
        /**
         * Return a connection to the pool for use by other requests.
         * @param sqlConnection SQL Connection being returned
         */ 
        public function returnConnection(sqlConnection:SQLConnection):void
        {
            delete _inUse[sqlConnection];
            _pool.push(sqlConnection);
            _useCount--;
            _myLogger.debug("Connection returned. Using {0} of {1}.", _useCount, _pool.length);
            processPending();
        }

        /**
         * Loop over all pending requests and either assign a request
         * an inactive connection or create a new connection if the
         * maximum pool size hasn't been reached yet.
         */ 
        private function processPending():void
        {
            while (_pending.length > 0)
            {
                var handler:Function;
                if (_pool.length > 0)
                {
                    var sqlConnection:SQLConnection = SQLConnection(_pool.shift());
                    _inUse[sqlConnection] = true;
                    _useCount++;                    
                    handler = _pending.shift() as Function;
                    handler(sqlConnection);
                }
                else if (_useCount < MAX_SIZE)
                {
                    handler = _pending.shift() as Function;
                    openConnection(handler);
                }
                else
                {
                    // Can't do anything now, wait for a connection
                    // to be returned.
                    return;
                }
            }
        }
        
        /**
         * Open a new connection. Once the connection is opened
         * pass it to the handler.
         * @param handler Function to get handler, or null if it is just precaching
         */ 
        private function openConnection(handler:Function = null):void
        {
            _useCount++;
            var sqlConnection:SQLConnection = new SQLConnection();
            sqlConnection.addEventListener(SQLErrorEvent.ERROR, function(sqlErrorEvent:SQLErrorEvent):void
            {
                // Basic error handling that just does error reporting
                // with no attempt at error recovery.
                _myLogger.error("Connection error {0}.", sqlErrorEvent);
                delete _inUse[sqlConnection];
                _useCount--;
                // At this point no reference to the sqlConnection should
                // exist and it can be GCed.
            });
            sqlConnection.addEventListener(SQLEvent.OPEN, function(sqlEvent:SQLEvent):void
            {
                sqlConnection.removeEventListener(SQLEvent.OPEN, arguments.callee);
                if (handler == null)
                {
                    returnConnection(sqlConnection);    
                }
                else
                {
                    handler(sqlConnection);
                }
            });
            // It is not recommended to open multiple connections to
            // the same database if you will be modifying data. Attempting
            // to read from a database at the same time an update operation
            // is taking place will result in an exception. To use UPDATE
            // or CREATE you should either reduce the connection pool
            // size to 1 or introduce a write barrier to queued operations
            // so that if a write operation is first in the queue it will
            // only execute when no connection is in use and while it is
            // executing no other queued operations can execute.
            // For queries that update the database one would also need
            // to account for starting and committing transactions and
            // how to handle any rollbacks.
            sqlConnection.openAsync(_database, SQLMode.UPDATE);
        }
 
        /**
         * Close a SQLConnection. This is only called from reset() so
         * no bookkeeping is done here as that function wipes out
         * all state.
         * @param sqlConnection Connection to close.
         */
        private function closeConnection(sqlConnection:SQLConnection):void
        {
            sqlConnection.addEventListener(SQLEvent.CLOSE, function(sqlEvent:SQLEvent):void
            {
                sqlConnection.removeEventListener(SQLEvent.CLOSE, arguments.callee);
                _myLogger.debug("Connection closed.");
            });
            sqlConnection.close();
        }
    }
}